package com.hmdp.service.impl;

import cn.hutool.core.date.TemporalUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.RedisData;
import com.hmdp.utils.StringRedisClient;
import com.hmdp.utils.SystemConstants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.domain.geo.GeoReference;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.GEO_SHOP_TYPE_ID;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private StringRedisClient stringRedisClient;
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * @param id
     * @return 先查redis 再查mysql
     * 缓存没有，查完mysql后保存redis中
     */
    @Override
    public Result queryById(Long id) {

        // 缓存穿透
//   Shop shop=    getShopWithCache(id);
        // 缓存击穿 互斥锁
//        Shop shop = getShopByIdAndLock(id);
        // 缓存击穿 逻辑过期
//        Shop shop = getShopWithLocalTime(id);

        // 工具类封装 缓存击穿 互斥锁
//        Shop shop = stringRedisClient.queryWithMutex(
//                RedisConstants.SHOP_ID,
//                RedisConstants.SHOP_LOCK,
//                id, Shop.class, this::getById,10L,TimeUnit.SECONDS);

        // 工具类封装 缓存击穿 逻辑过期
        Shop shop = stringRedisClient.queryWithLogicalExpire(
                RedisConstants.SHOP_TiME_ID,
                RedisConstants.SHOP_LOCK,
                id, Shop.class, this::getById, 10L,TimeUnit.MINUTES);
        return Result.ok(shop);
    }

    /**
     * 建立缓存，解决了缓存穿透,未解决缓存击穿
     *
     * @param id
     * @return
     */
    private Shop getShopWithCache(Long id) {
        String key = RedisConstants.SHOP_ID + id;
        // 1.查缓存
        String shop = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(shop)) {
            // 1.1存在 直接返回
            Shop bean = JSONUtil.toBean(shop, Shop.class);
            return bean;
        }
        // 1.2是否是空值
        if (shop != null) {
            return null;
        }
        // 2.不存在查数据库
        Shop bean = this.getById(id);
        if (bean == null) {
            // 2.1数据库不存在 报错 写入空值解决缓存穿透
            stringRedisTemplate.opsForValue().set(key, "");
            return null;
        }
        // 3.保存缓存
        String beanJson = JSONUtil.toJsonStr(bean);
        stringRedisTemplate.opsForValue().set(key, beanJson, 10, TimeUnit.MINUTES);
        // 4.返回
        return bean;
    }

    /**
     * 解决 缓存穿透 ，缓存击穿
     *
     * @param id
     * @return
     */
    private Shop getShopByIdAndLock(Long id) {
        String key = RedisConstants.SHOP_ID + id;
        // 1.查缓存
        String shop = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(shop)) {
            // 1.1存在 直接返回
            Shop bean = JSONUtil.toBean(shop, Shop.class);
            return bean;
        }
        // 1.2是否是空值
        if (shop != null) {
            return null;
        }
        // 2.不存在查数据库
        // 2.1获取锁
        String lockKey = RedisConstants.SHOP_LOCK + id;
        Boolean aBoolean = tryLock(lockKey);
        Shop bean = null;
        try {
            if (!aBoolean) {
                // 2.2失败 睡眠再重试
                Thread.sleep(10);
                return getShopByIdAndLock(id);
            }
            else {
                // 2.3成功 重建缓存
                bean = this.getById(id);
                if (bean == null) {
                    // 2.4数据库不存在 报错 写入空值解决缓存穿透
                    stringRedisTemplate.opsForValue().set(key, "");
                    return null;
                }
                // 3.保存缓存
                String beanJson = JSONUtil.toJsonStr(bean);
                stringRedisTemplate.opsForValue().set(key, beanJson, 10, TimeUnit.MINUTES);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException();
        } finally {
            // 3.释放锁
            deleteLock(lockKey);
        }
        // 4.返回数据
        return bean;
    }

    /**
     * 获取锁
     *
     * @param key
     * @return
     */
    private Boolean tryLock(String key) {
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(aBoolean);
    }
    /**
     * 释放锁
     *
     * @param key
     */
    private void deleteLock(String key) {
        stringRedisTemplate.delete(key);
    }
    /**
     * 逻辑过期
     * @param id
     * @return
     */
    private Shop getShopToLocalTime(Long id){
        String key = RedisConstants.SHOP_ID + id;
        // 1.查缓存
        String shop = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(shop)) {
            // 1.1存在 直接返回
            Shop bean = JSONUtil.toBean(shop, Shop.class);
            return bean;
        }
        // 1.2是否是空值
        if (shop != null) {
            return null;
        }
        // 2.不存在查数据库
        Shop bean = this.getById(id);
        if (bean == null) {
            // 2.1数据库不存在 报错 写入空值解决缓存穿透
            stringRedisTemplate.opsForValue().set(key, "");
            return null;
        }
        // 3.保存缓存
        String beanJson = JSONUtil.toJsonStr(bean);
        stringRedisTemplate.opsForValue().set(key, beanJson, 10, TimeUnit.MINUTES);
        // 4.返回
        return bean;
    }

    /**
     * 封装逻辑过期 数据预热
     *
     * @param <T>
     * @param t
     * @param key
     * @param time
     * @param unit
     */
    @Override
    public  <T> void setRedisDateLocalTime(T t, String key, Long time, TimeUnit unit) {
        RedisData<T> tRedisData = new RedisData<>();
        tRedisData.setData(t);
        tRedisData.setExpireTime(LocalDateTime.now().plus(time, TemporalUtil.toChronoUnit(unit)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(tRedisData));
    }

    /**
     * 查询店铺信息与距离
     * @param typeId 店铺类型ID
     * @param current 当前页
     * @param x 经度
     * @param y 纬度
     * @return 商品列表
     */
    @Override
    public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
        //判断是否需要地理位置
        if (x == null || y == null) {
            // 不需要 普通查询
            Page<Shop> page = page(new Page<>(current,
                            SystemConstants.DEFAULT_PAGE_SIZE),
                    Wrappers.lambdaQuery(Shop.class).eq(Shop::getTypeId, typeId));
            return Result.ok(page.getRecords());
        }
        //计算分页参数
        // 起始位置
        // 2.计算分页参数
        int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
        int end = current * SystemConstants.DEFAULT_PAGE_SIZE;

        //查询地理位置距离
        String key = GEO_SHOP_TYPE_ID+typeId;
        GeoResults<RedisGeoCommands.GeoLocation<String>> search = stringRedisTemplate.opsForGeo().search(
                key,
                // 当前位置
                GeoReference.fromCoordinate(x, y),
                // 最近距离
                new Distance(50000),
                // 包含信息与限制个数
                RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end));
        if(search == null){
            // 返回空
            return Result.ok(Collections.emptyList());
        }
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> content = search.getContent();
        if(from > content.size()){
            // 没有下一页 返回空
            return Result.ok(Collections.emptyList());
        }
        // 创建hashMap集合 方便添加距离
        HashMap<Long, Double> hashMap = new HashMap<>(content.size());
        // 创建list集合 保证查询结果id的有序性
        List<String> idList = new ArrayList<>(content.size());
        // 截断 实现分页 skip() 获取店铺id
        content.stream()
                .skip(from)
                .forEach(o-> {
            hashMap.put(Long.valueOf(o.getContent().getName()),o.getDistance().getValue());
            idList.add(o.getContent().getName());
        });
        // 根据店铺id查询信息 并添加上距离信息
        // Set<Long> ids = hashMap.keySet(); set集合无序 无法保证id的有序性
        String idStr = StrUtil.join(",", idList);
        // 获取店铺列表
        List<Shop> shops = query().inSql("id", idStr).last("ORDER BY FIELD(id," + idStr + ")").list();
        // 添加距离
        shops.forEach(shop -> {
            shop.setDistance(hashMap.get(shop.getId()));
        });
        // 返回
        return Result.ok(shops);
    }

    /**
     * 逻辑过期 解决缓存击穿 不设置缓存的ttl
     * @param id
     * @return
     */
    private Shop getShopWithLocalTime(Long id) {
        String key = RedisConstants.SHOP_ID + id;
        // 1.查缓存
        String shop = stringRedisTemplate.opsForValue().get(key);
        // 2. 是否为空 (null 与 “”) 直接返回 不是热点key(数据已经预热即缓存一定存在)或 缓存穿透
        if (StrUtil.isBlank(shop)) {
            return null;
        }
        // 3.判断逻辑过期
        // 反序列化
        RedisData redisData = JSONUtil.toBean(shop, RedisData.class);
        String shopStr = JSONUtil.toJsonStr(redisData.getData());
        Shop shop1 = JSONUtil.toBean(shopStr, Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        if(expireTime.isAfter(LocalDateTime.now())){
            // 3.1未过期 直接返回
            return shop1;
        }
        // 3.2 过期 重建缓存
        // 4. 获取锁
        String lockKey= RedisConstants.SHOP_LOCK+id;
        Boolean aBoolean = tryLock(lockKey);
        if( aBoolean){
            // 4.1 获取锁成功 开启另一个线程重建缓存
            CACHE_REBUILD_EXECUTOR.submit(()->{
                // 4.2 获取锁成功 开启另一个线程重建缓存
                try {
                    Shop bean = this.getById(id);
                    if (bean == null) {
                        // 缓存穿透 写空值
                        stringRedisTemplate.opsForValue().set(key, "");
                    }
                    // 3.保存缓存
                   setRedisDateLocalTime(bean, key, 10L, TimeUnit.SECONDS);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    // 5.释放锁
                    deleteLock(lockKey);
                }
            });
        }
        // 6.获取锁失败返回旧值
        return shop1;
    }
    /**
     * 数据一致性 修改店铺信息
     *
     * @param shop
     * @return
     */
    @Transactional
    @Override
    public Result updateShopDate(Shop shop) {

        // 数据校验
        if (shop.getId() == null) {
            return Result.fail("店铺id为空");
        }
        // 1. 先更新数据库
        String key = RedisConstants.SHOP_ID + shop.getId();
        updateById(shop);
        // 2. 删除缓存 （写入缓存由第一次查询执行）
        stringRedisTemplate.delete(key);
        return Result.ok("更新成功");
    }
}
